<?php
/**
 * Вспомогательные функции для работы с DOM-деревом
 *
 * @ingroup Helpers
 */
class Helper_Dom {
    
    /**
     * Преобразовать HTML-текст в DOMDocument
     * 
     * @param Strng $html
     * @return DOMDocument
     */
    public function getDOMDoc( $html ) {
        $config = array('output-xhtml'   => TRUE);
        $tidy = new tidy();
        $tidy->parseString($html, $config, 'UTF8');
        $tidy->cleanRepair();
        $html = $tidy;

        $doc = new DOMDocument();
        $doc->preserveWhiteSpace = FALSE;
        @$doc->loadHTML($html);

        return $doc;
    }

    public function getDeepLevel( $node ) {
        $count = 1;

        if ( !$this->isElementNode($node) ||  $node->childNodes->length == 0 ) {
            return $count;
        }

        $childNode = $node->firstChild;
        while( $childNode ) {
            if ( $childNode->childNodes !== NULL ) {
                $count += $this->getDeepLevel( $childNode );
            }

            $count += 1;
            $childNode = $childNode->nextSibling;
        }

        return $count;
    }

    public function isElementNode($node) {
        return $node->nodeType == XML_ELEMENT_NODE;
    }

    /**
     * Получить наименее близкий родительский узел для массива узлов
     *
     * @param Array $nodes
     * @return DOMElement
     */
    public function getNearSameParent( array $nodes ) {
        $nearestNodes = array();

        function find( $nodes, $node ) {
            foreach( $nodes as $idx => $el ) {
                if ( $el['node'] == $node ) {
                    return $idx;
                }
            }

            return -1;
        }

        for ( $i = 0; $i < count($nodes); $i++ ) {
            $parent = NULL;

            if ( !isset($nodes[$i+1] ) ) {
                break;
            }
            
            $parent = $_fparentNode = $nodes[$i];
            $_sparentNode = $nodes[$i+1];
            while ( $_sparentNode && $_fparentNode && !$_fparentNode->isSameNode( $_sparentNode ) ) {
                $_fparentNode = $_fparentNode->parentNode;
                $_sparentNode = $_sparentNode->parentNode;
                
                $parent = $_fparentNode;
            }
            

             if ( -1 != ( $idx = find( $nearestNodes, $parent) ) ) {
                $nearestNodes[$idx]['count']++;
            } else {
                $nearestNodes[] = array(
                    "node" => $parent,
                    "count" => 1
                );
            }
        }

        usort(&$nearestNodes, create_function("\$node, \$node1", "return (\$node['count'] == \$node1['count'] ? 0 : (\$node['count'] < \$node1['count'] ? 1 : -1 ));") );

        $el = array_shift($nearestNodes);
        return $el['node'];
    }

    /**
     * Получить номер узла в родительском узле, по отношению к другим
     * узлам с таким же именем.
     * Используетя при построении XPath пути к элементу.
     *
     * @todo Почему-то не работает правильно
     *
     * @param DOMElement $parent
     * @param DOMElement $node
     * @return String
     */
    public function getNodePosition( $parent, $node ) {
        $pos = 0;
        $firstSibling = $parent->firstChild;
        while ( $firstSibling != $parent->lastChild && $firstSibling !== $node ) {
            if ( $firstSibling->nodeName == $node->nodeName ) {
                $pos++;
            }

            $firstSibling = $firstSibling->nextSibling ;
        }

        return $pos;
    }

        /**
     * Построить XPath-путь к некоторому узлу дерева
     *
     * @param DOMElement $node
     * @return String
     */
    public function buildXPath( $node ) {
        $map = $node->nodeName . "[". ( $this->getNodePosition($node->parentNode, $node) )."]/";

        $parentNode = $node->parentNode;
        while ( $parentNode ) {
            $map .= $parentNode->nodeName;
            if ( $parentNode->parentNode ) {
                $map .= "[". $this->getNodePosition( $parentNode->parentNode, $parentNode ) ."]/";
            }

            $parentNode = $parentNode->parentNode;
        }

        return join("/", array_reverse( explode("/", $map) ) );

    }

    /**
     * Содержит ли данный узел потомков с указанным именем
     *
     * @param DOMElement $parent
     * @param String $name
     * @param bool $lookUpTree
     * @return bool
     */
    public function findChildByName( $parent, $name, $lookUpTree = FALSE ) {
        $is_parent = FALSE;

        $firstChild = $parent->firstChild;
        while( $firstChild ) {
            if ( $firstChild->nodeName == $name ) {
                $is_parent = TRUE;
                break;
            }

            if ( $lookUpTree && $this->isElementNode($firstChild) ) {
                if ( $is_parent = $this->findChildByName( $firstChild, $name, TRUE) ) {
                    break;
                }
            }

            $firstChild = $firstChild->nextSibling;
        }

        return $is_parent;
    }

    /**
     * Является ли данный узел родителем по отношнию к узлу $node
     *
     * @param DOMElement $parent
     * @param DOMElement $node
     * @param Искать вглубину $lookUpTree
     * @return bool
     */
    public function isParent( $parent, $node, $lookUpTree = FALSE ) {
        if ( !$lookUpTree) {
            if ( is_array($node) ) {
                foreach( $node as $sibling ) {
                    if ( !$this->isParent( $parent, $sibling, $lookUpTree) ) {
                        return false;
                    }
                }
                return;
            }
            
            return isset($node->parentNode) ? $parent === $node->parentNode : FALSE;
        } else {
            $nodeParent = $node->parentNode;
            while ( $nodeParent ) {
                if ( $nodeParent === $parent ) {
                    return TRUE;
                }

                $nodeParent = $nodeParent->parentNode;
            }
        }

        return FALSE;
    }

    /**
     * Получить список узлов находящихся между узлами $start и $end, с ограничением по высоте
     * в качестве элемента $topLimitEl
     *
     * @param DOMElement $start
     * @param DOMElement $end
     * @param DOMElement $topLimitEl
     * @return Array
     */
    public function getElementsBetween( DOMElement $start, DOMElement $end, DOMElement $topLimitEl = NULL ) {
        $nodes = array();

        // Достигнута ли позиция элемента, с которого начинается выборка
        $_startPos = FALSE;
        // Выбираем элементы на одном уровне со $start
        foreach ( $start->parentNode->childNodes as $_targetEl  ) {
            if ( $_targetEl->isSameNode( $end ) ) {
                break;
            }

            if ( $_targetEl->isSameNode( $start ) ) {
                $_startPos = TRUE;
                continue;
            }

            if ( $_startPos ) {
                if ( $_targetEl instanceof DOMText ) {
                    if ( $_targetEl->nextSibling instanceof DOMElement ) {
                        $_targetEl = $_targetEl->nextSibling;
                    }
                }
                $nodes[] = $_targetEl;
            }
        }

        return $this->plainificate( $nodes );
    }

    public function plainificate( $node ) {
        $result = array();
        if ( is_array( $node ) ) {
            foreach( $node as $list_item ) {
                $result = array_merge( $result, $this->plainificate( $list_item ) );
            }
        } else {
            if ( empty($node->childNodes) || !($node instanceof DOMElement ) )  {
                $result [] = $node;
            } else {
                foreach( $node->childNodes as $list_item ) {
                    if ( $list_item instanceof DOMElement ) {
                        $result = array_merge( $result, $this->plainificate( $list_item ) );
                    }
                }

                $result [] = $node;
            }
        }

        return $result;
    }
}